/*
This file is part of the 3dengfx, realtime visualization system.

Copyright (c) 2004, 2005 John Tsiombikas <nuclear@siggraph.org>

3dengfx is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

3dengfx is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with 3dengfx; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/* main 3dengfx state control, and low level OpenGL interaction
 *
 * Author: John Tsiombikas 2004
 */

#include "3dengfx_config.h"

#include <iostream>
#include <list>
#include <csignal>
#include "opengl.h"
#include "fxwt/fxwt.hpp"
#include "fxwt/init.hpp"
#include "fxwt/gfx_library.h"
#include "3denginefx.hpp"
#include "texman.hpp"
#include "sdrman.hpp"
#include "camera.hpp"
#include "gfx/3dgeom.hpp"
#include "gfxprog.hpp"
#include "gfx/image.h"
#include "common/config_parser.h"
#include "common/err_msg.h"

using std::cout;
using std::cerr;
using std::endl;
using std::string;

#ifdef SINGLE_PRECISION_MATH
#define GL_SCALAR_TYPE	GL_FLOAT
#else
#define GL_SCALAR_TYPE	GL_DOUBLE
#endif	// SINGLE_PRECISION_MATH


void (*load_matrix_gl)(const Matrix4x4 &mat);

namespace glext {
#ifdef SINGLE_PRECISION_MATH
	PFNGLLOADTRANSPOSEMATRIXFARBPROC glLoadTransposeMatrix;
#else
	PFNGLLOADTRANSPOSEMATRIXDARBPROC glLoadTransposeMatrix;
#endif	// SINGLE_PRECISION_MATH

	PFNGLACTIVETEXTUREARBPROC glActiveTexture;
	PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTexture;

	PFNGLBINDBUFFERARBPROC glBindBuffer;
	PFNGLBUFFERDATAARBPROC glBufferData;
	PFNGLDELETEBUFFERSARBPROC glDeleteBuffers;
	PFNGLISBUFFERARBPROC glIsBuffer;
	PFNGLMAPBUFFERARBPROC glMapBuffer;
	PFNGLUNMAPBUFFERARBPROC glUnmapBuffer;
	PFNGLGENBUFFERSARBPROC glGenBuffers;

	// fragment/vertex program extensions
	PFNGLBINDPROGRAMARBPROC glBindProgram;
	PFNGLGENPROGRAMSARBPROC glGenPrograms;
	PFNGLDELETEPROGRAMSARBPROC glDeletePrograms;
	PFNGLPROGRAMSTRINGARBPROC glProgramString;

	// point parameters
	PFNGLPOINTPARAMETERFARBPROC glPointParameterf;
	PFNGLPOINTPARAMETERFVARBPROC glPointParameterfv;

	// --- OpenGL 2.0 Shading Language ---
	
	// - objects
	PFNGLDELETEOBJECTARBPROC glDeleteObject;
	PFNGLATTACHOBJECTARBPROC glAttachObject;
	PFNGLDETACHOBJECTARBPROC glDetachObject;
	PFNGLGETOBJECTPARAMETERIVARBPROC glGetObjectParameteriv;
	PFNGLGETINFOLOGARBPROC glGetInfoLog;

	// - program objects
	PFNGLCREATEPROGRAMOBJECTARBPROC glCreateProgramObject;
	PFNGLLINKPROGRAMARBPROC glLinkProgram;
	PFNGLUSEPROGRAMOBJECTARBPROC glUseProgramObject;

	// - shader objects
	PFNGLCREATESHADEROBJECTARBPROC glCreateShaderObject;
	PFNGLSHADERSOURCEARBPROC glShaderSource;
	PFNGLCOMPILESHADERARBPROC glCompileShader;

	// - uniforms
	PFNGLGETUNIFORMLOCATIONARBPROC glGetUniformLocation;
	PFNGLGETACTIVEUNIFORMARBPROC glGetActiveUniform;
	PFNGLUNIFORM1FARBPROC glUniform1f;
	PFNGLUNIFORM2FARBPROC glUniform2f;
	PFNGLUNIFORM3FARBPROC glUniform3f;
	PFNGLUNIFORM4FARBPROC glUniform4f;
	PFNGLUNIFORMMATRIX3FVARBPROC glUniformMatrix3fv;
	PFNGLUNIFORMMATRIX4FVARBPROC glUniformMatrix4fv;
}

using namespace glext;

static const char *gl_error_string[] = {
	"GL_INVALID_ENUM",		// 0x500
	"GL_INVALID_VALUE",		// 0x501
	"GL_INVALID_OPERATION",	// 0x502
	"GL_STACK_OVERFLOW",	// 0x503
	"GL_STACK_UNDERFLOW",	// 0x504
	"GL_OUT_OF_MEMORY",		// 0x505
	"GL_NO_ERROR",			// 0x0
	"[INVALID ERROR NUMBER]"
};

///////////////// local 3d engine state block ///////////////////
static bool gc_valid;
static GraphicsInitParameters gparams;
static Matrix4x4 tex_matrix[8];
static int coord_index[MAX_TEXTURES];
static PrimitiveType primitive_type;
static StencilOp stencil_fail, stencil_pass, stencil_pzfail;
static int stencil_ref;
static bool mipmapping = true;
static TextureDim ttype[8];	// the type of each texture bound to each texunit (1D/2D/3D/CUBE)

namespace engfx_state {
	SysCaps sys_caps;
	Matrix4x4 world_matrix;
	Matrix4x4 view_matrix, inv_view_matrix;
	const Camera *view_mat_camera;
	Matrix4x4 proj_matrix;
	const Light *bump_light;
	int light_count;
}

using namespace engfx_state;

GraphicsInitParameters *load_graphics_context_config(const char *fname) {
	static GraphicsInitParameters gip;	
	gip.x = 640;
	gip.y = 480;
	gip.bpp = 16;
	gip.depth_bits = 16;
	gip.stencil_bits = 8;
	gip.dont_care_flags = 0;

	set_log_filename("3dengfx.log");
	set_verbosity(2);
	
	if(load_config_file(fname) == -1) {
		error("%s: could not load config file", __func__);
		return 0;
	}
	
	const ConfigOption *cfgopt;
	while((cfgopt = get_next_option())) {
		
		if(!strcmp(cfgopt->option, "fullscreen")) {
			if(!strcmp(cfgopt->str_value, "true")) {
				gip.fullscreen = true;
			} else if(!strcmp(cfgopt->str_value, "false")) {
				gip.fullscreen = false;
			} else {
				error("%s: error parsing config file %s", __func__, fname);
				return 0;
			}
		} else if(!strcmp(cfgopt->option, "resolution")) {
			if(!isdigit(cfgopt->str_value[0])) {
				error("%s: error parsing config file %s", __func__, fname);
				return 0;
			}
			gip.x = atoi(cfgopt->str_value);
			
			char *ptr = cfgopt->str_value;
			while(*ptr && *ptr != 'x') *ptr++;
			if(!*ptr || !*(ptr+1) || !isdigit(*(ptr+1))) {
				error("%s: error parsing config file %s", __func__, fname);
				return 0;
			}
			
			gip.y = atoi(ptr + 1);
		} else if(!strcmp(cfgopt->option, "bpp")) {
			if(cfgopt->flags & CFGOPT_INT) {
				gip.bpp = cfgopt->int_value;
			} else if(!strcmp(cfgopt->str_value, "dontcare")) {
				gip.bpp = 32;
				gip.dont_care_flags |= DONT_CARE_BPP;
			} else {
				error("%s: error parsing config file %s", __func__, fname);
				return 0;
			}			
		} else if(!strcmp(cfgopt->option, "zbuffer")) {
			if(cfgopt->flags & CFGOPT_INT) {
				gip.depth_bits = cfgopt->int_value;
			} else if(!strcmp(cfgopt->str_value, "dontcare")) {
				gip.depth_bits = 32;
				gip.dont_care_flags |= DONT_CARE_DEPTH;
			} else {
				error("%s: error parsing config file %s", __func__, fname);
				return 0;
			}
		} else if(!strcmp(cfgopt->option, "stencil")) {
			if(cfgopt->flags & CFGOPT_INT) {
				gip.stencil_bits = cfgopt->int_value;
			} else if(!strcmp(cfgopt->str_value, "dontcare")) {
				gip.stencil_bits = 8;
				gip.dont_care_flags |= DONT_CARE_STENCIL;
			} else {
				error("%s: error parsing config file %s", __func__, fname);
				return 0;
			}
		}
	}
	
	destroy_config_parser();
	
	return &gip;		
}

/* ---- get_system_capabilities() ----
 * Retrieves information on the graphics subsystem capabilities
 * and returns a SysCaps structure describing them
 */
SysCaps get_system_capabilities() {
	static bool first_call = true;
	
	if(!first_call) {
		return sys_caps;
	}
	first_call = false;
	
	// get extensions & vendor strings
	const char *tmp_str = (const char*)glGetString(GL_EXTENSIONS);
	if(!tmp_str) {
		error("%s: glGetString() failed, possibly no valid GL context", __func__);
		exit(-1);
	}
	char *ext_str = new char[strlen(tmp_str) + 1];
	strcpy(ext_str, tmp_str);
	
	char *cptr = ext_str;	
	while(*cptr) {
		if(*cptr == ' ') *cptr = '\n';
		cptr++;
	}

	set_log_filename("gl_ext.log");
	info("Supported extensions:\n-------------\n%s", ext_str);
	set_log_filename("3dengfx.log");
		
	info("Rendering System Information:");

	const char *vendor = (const char*)glGetString(GL_VENDOR);
	info("  Vendor: %s", vendor);
	info("Renderer: %s", glGetString(GL_RENDERER));
	info(" Version: %s", glGetString(GL_VERSION));
	info("(note: the list of extensions is logged seperately at \"gl_ext.log\")");

	// fill the SysCaps structure
	//SysCaps sys_caps;
	sys_caps.load_transpose = (bool)strstr(ext_str, "GL_ARB_transpose_matrix");
	sys_caps.gen_mipmaps = (bool)strstr(ext_str, "GL_SGIS_generate_mipmap");
	sys_caps.tex_combine_ops = (bool)strstr(ext_str, "GL_ARB_texture_env_combine");
	sys_caps.bump_dot3 = (bool)strstr(ext_str, "GL_ARB_texture_env_dot3");
	sys_caps.bump_env = (bool)strstr(ext_str, "GL_ATI_envmap_bumpmap");
	sys_caps.vertex_buffers = (bool)strstr(ext_str, "GL_ARB_vertex_buffer_object");
	sys_caps.depth_texture = (bool)strstr(ext_str, "GL_ARB_depth_texture");
	sys_caps.shadow_mapping = (bool)strstr(ext_str, "GL_ARB_shadow");
	sys_caps.point_sprites = (bool)strstr(ext_str, "GL_ARB_point_sprites");
	sys_caps.point_params = (bool)strstr(ext_str, "GL_ARB_point_parameters");
	glGetIntegerv(GL_MAX_TEXTURE_UNITS_ARB, &sys_caps.max_texture_units);
	
	sys_caps.prog.asm_vertex = (bool)strstr(ext_str, "GL_ARB_vertex_program");
	sys_caps.prog.asm_pixel = (bool)strstr(ext_str, "GL_ARB_fragment_program");
	sys_caps.prog.glslang = (bool)strstr(ext_str, "GL_ARB_shading_language_100");
	sys_caps.prog.shader_obj = (bool)strstr(ext_str, "GL_ARB_shader_objects");
	sys_caps.prog.glsl_vertex = (bool)strstr(ext_str, "GL_ARB_vertex_shader");
	sys_caps.prog.glsl_pixel = (bool)strstr(ext_str, "GL_ARB_fragment_shader");

	delete [] ext_str;
	
	// also log these things
	info("-------------------");
	info("System Capabilities");
	info("-------------------");
	info("Load transposed matrices: %s", sys_caps.load_transpose ? "yes" : "no");
	info("Auto-generate mipmaps (SGIS): %s", sys_caps.gen_mipmaps ? "yes" : "no");
	info("Custom texture combination operations: %s", sys_caps.tex_combine_ops ? "yes" : "no");
	info("Diffuse bump mapping (dot3): %s", sys_caps.bump_dot3 ? "yes" : "no");
	info("Specular bump mapping (env-bump): %s", sys_caps.bump_env ? "yes" : "no");
	info("Video memory vertex/index buffers: %s", sys_caps.vertex_buffers ? "yes" : "no");
	info("Depth texture: %s", sys_caps.depth_texture ? "yes" : "no");
	info("Shadow mapping: %s", sys_caps.shadow_mapping ? "yes" : "no");
	info("Programmable vertex processing (asm): %s", sys_caps.prog.asm_vertex ? "yes" : "no");
	info("Programmable pixel processing (asm): %s", sys_caps.prog.asm_pixel ? "yes" : "no");
	info("OpenGL 2.0 shading language: %s", sys_caps.prog.glslang ? "yes" : "no");
	info("Programmable vertex processing (glsl): %s", sys_caps.prog.glsl_vertex ? "yes" : "no");
	info("Programmable pixel processing (glsl): %s", sys_caps.prog.glsl_pixel ? "yes" : "no");
	info("Point sprites: %s", sys_caps.point_sprites ? "yes" : "no");
	info("Point parameters: %s", sys_caps.point_params ? "yes" : "no");
	info("Texture units: %d", sys_caps.max_texture_units);

	if(!sys_caps.point_sprites && !sys_caps.point_params) {
		warning("no point sprites support, falling back to billboards which *may* degrade particle system performance");
	}

	return sys_caps;
}

const char *get_glerror_string(GLenum error) {
	if(!error) return gl_error_string[0x506];
	if(error < 0x500 || error > 0x505) error = 0x507;
	return gl_error_string[error - 0x500];
}

/* load_matrix_transpose_arb() & load_matrix_transpose_manual()
 * --------------------------------------------------------
 * two functions to handle the transformation matrix loading
 * to OpenGL by either transposing the Matrix4x4 data or using
 * the transposed-loading extension (use through function pointer
 * LoadMatrixGL which is set during initialization to the correct one)
 */
void load_matrix_transpose_arb(const Matrix4x4 &mat) {
	glLoadTransposeMatrix(mat.opengl_matrix());
}

void load_matrix_transpose_manual(const Matrix4x4 &mat) {
#ifdef SINGLE_PRECISION_MATH
	glLoadMatrixf(mat.transposed().opengl_matrix());
#else
	glLoadMatrixd(mat.transposed().opengl_matrix());
#endif	// SINGLE_PRECISION_MATH
}


//////////////// 3D Engine Initialization ////////////////

static void signal_handler(int sig) {
	error("It seems this is the end... caught signal %d, exiting...\n", sig);
	destroy_graphics_context();
	exit(-1);
}

/* ---- create_graphics_context() ----
 * initializes the graphics subsystem according to the init parameters
 */
bool create_graphics_context(const GraphicsInitParameters &gip) {
	
	gparams = gip;

	remove("3dengfx.log");
	remove("gl_ext.log");

	set_log_filename("3dengfx.log");
	set_verbosity(2);

	if(!fxwt::init_graphics(&gparams)) {
		return false;
	}

	signal(SIGSEGV, signal_handler);
	signal(SIGILL, signal_handler);
	signal(SIGTERM, signal_handler);
	signal(SIGFPE, signal_handler);
	signal(SIGINT, signal_handler);

#if GFX_LIBRARY == GTK
	fxwt::init();
	return true;
#else
	if(!start_gl()) return false;
	fxwt::init();
	return true;
#endif	// GTK
}

/*
 * short graphics context creation
 * creates a graphics context (windowed or fullscreen)
 * given only the wanted resolution and a fullscreen flag.
 */
bool create_graphics_context(int x, int y, bool fullscreen)
{
	GraphicsInitParameters gip;
	gip.x = x;
	gip.y = y;
	gip.bpp = 32;
	gip.depth_bits = 32;
	gip.fullscreen = fullscreen;
	gip.stencil_bits = 8;
	gip.dont_care_flags = DONT_CARE_DEPTH | DONT_CARE_STENCIL | DONT_CARE_BPP;

	return create_graphics_context(gip);
}

/* OpenGL startup after initialization */
bool start_gl() {
	SysCaps sys_caps = get_system_capabilities();
	if(sys_caps.max_texture_units < 2) {
		error("%s: Your system does not meet the minimum requirements (at least 2 texture units)", __func__);
		return false;
	}

	glext::glActiveTexture = (PFNGLACTIVETEXTUREARBPROC)glGetProcAddress("glActiveTextureARB");
	glext::glClientActiveTexture = (PFNGLCLIENTACTIVETEXTUREARBPROC)glGetProcAddress("glClientActiveTextureARB");
	
	if(!glext::glActiveTexture || !glext::glClientActiveTexture) {
		error("%s: OpenGL implementation less than 1.3 and could not load multitexturing ARB extensions", __func__);
		return false;
	}

	if(sys_caps.load_transpose) {
#ifdef SINGLE_PRECISION_MATH
		glLoadTransposeMatrix = (PFNGLLOADTRANSPOSEMATRIXFARBPROC)glGetProcAddress("glLoadTransposeMatrixfARB");
#else
		glLoadTransposeMatrix = (PFNGLLOADTRANSPOSEMATRIXDARBPROC)glGetProcAddress("glLoadTransposeMatrixdARB");
#endif	// SINGLE_PRECISION_MATH
		
		load_matrix_gl = load_matrix_transpose_arb;
	} else {
		load_matrix_gl = load_matrix_transpose_manual;
	}

	if(sys_caps.vertex_buffers) {
		glBindBuffer = (PFNGLBINDBUFFERARBPROC)glGetProcAddress("glBindBufferARB");
		glBufferData = (PFNGLBUFFERDATAARBPROC)glGetProcAddress("glBufferDataARB");
		glDeleteBuffers = (PFNGLDELETEBUFFERSARBPROC)glGetProcAddress("glDeleteBuffersARB");
		glIsBuffer = (PFNGLISBUFFERARBPROC)glGetProcAddress("glIsBufferARB");
		glMapBuffer = (PFNGLMAPBUFFERARBPROC)glGetProcAddress("glMapBufferARB");
		glUnmapBuffer = (PFNGLUNMAPBUFFERARBPROC)glGetProcAddress("glUnmapBufferARB");
		glGenBuffers = (PFNGLGENBUFFERSARBPROC)glGetProcAddress("glGenBuffersARB");
	}

	if(sys_caps.prog.asm_vertex || sys_caps.prog.asm_pixel) {
		glBindProgram = (PFNGLBINDPROGRAMARBPROC)glGetProcAddress("glBindProgramARB");
		glGenPrograms = (PFNGLGENPROGRAMSARBPROC)glGetProcAddress("glGenProgramsARB");
		glDeletePrograms = (PFNGLDELETEPROGRAMSARBPROC)glGetProcAddress("glDeleteProgramsARB");
		glProgramString = (PFNGLPROGRAMSTRINGARBPROC)glGetProcAddress("glProgramStringARB");
	}

	if(sys_caps.prog.shader_obj) {
		glDeleteObject = (PFNGLDELETEOBJECTARBPROC)glGetProcAddress("glDeleteObjectARB");
		glAttachObject = (PFNGLATTACHOBJECTARBPROC)glGetProcAddress("glAttachObjectARB");
		glDetachObject = (PFNGLDETACHOBJECTARBPROC)glGetProcAddress("glDetachObjectARB");
		glGetObjectParameteriv = (PFNGLGETOBJECTPARAMETERIVARBPROC)glGetProcAddress("glGetObjectParameterivARB");
		glGetInfoLog = (PFNGLGETINFOLOGARBPROC)glGetProcAddress("glGetInfoLogARB");
		
		glCreateProgramObject = (PFNGLCREATEPROGRAMOBJECTARBPROC)glGetProcAddress("glCreateProgramObjectARB");
		glLinkProgram = (PFNGLLINKPROGRAMARBPROC)glGetProcAddress("glLinkProgramARB");
		glUseProgramObject = (PFNGLUSEPROGRAMOBJECTARBPROC)glGetProcAddress("glUseProgramObjectARB");

		glCreateShaderObject = (PFNGLCREATESHADEROBJECTARBPROC)glGetProcAddress("glCreateShaderObjectARB");
		glShaderSource = (PFNGLSHADERSOURCEARBPROC)glGetProcAddress("glShaderSourceARB");
		glCompileShader = (PFNGLCOMPILESHADERARBPROC)glGetProcAddress("glCompileShaderARB");
		
		glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONARBPROC)glGetProcAddress("glGetUniformLocationARB");
		glGetActiveUniform = (PFNGLGETACTIVEUNIFORMARBPROC)glGetProcAddress("glGetActiveUniformARB");
		glUniform1f = (PFNGLUNIFORM1FARBPROC)glGetProcAddress("glUniform1fARB");
		glUniform2f = (PFNGLUNIFORM2FARBPROC)glGetProcAddress("glUniform2fARB");
		glUniform3f = (PFNGLUNIFORM3FARBPROC)glGetProcAddress("glUniform3fARB");
		glUniform4f = (PFNGLUNIFORM4FARBPROC)glGetProcAddress("glUniform4fARB");
		glUniformMatrix3fv = (PFNGLUNIFORMMATRIX3FVARBPROC)glGetProcAddress("glUniformMatrix3fvARB");
		glUniformMatrix4fv = (PFNGLUNIFORMMATRIX4FVARBPROC)glGetProcAddress("glUniformMatrix4fvARB");
	}
	
	if(sys_caps.point_params) {
		glPointParameterf = (PFNGLPOINTPARAMETERFARBPROC)glGetProcAddress("glPointParameterfARB");
		glPointParameterfv = (PFNGLPOINTPARAMETERFVARBPROC)glGetProcAddress("glPointParameterfvARB");

		if(!glPointParameterfv) {
			error("error loading glPointParameterfv");
			return false;
		}
		if(!glPointParameterf) {
			error("error loading glPointParameterf");
			return false;
		}
	}

	gc_valid = true;
	set_verbosity(3);
	
	set_default_states();
	return true;
}

void destroy_graphics_context() {
	if(!gc_valid) return;
	gc_valid = false;
	info("3d engine shutting down...");
	destroy_textures();
	destroy_shaders();
	fxwt::destroy_graphics();
}

void set_default_states() {
	set_primitive_type(TRIANGLE_LIST);
	set_front_face(ORDER_CW);
	set_backface_culling(true);
	set_zbuffering(true);
	set_lighting(true);
	set_auto_normalize(false);
	
	glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, 1);
	glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR);
	
	set_matrix(XFORM_WORLD, Matrix4x4());
	set_matrix(XFORM_VIEW, Matrix4x4());
	set_matrix(XFORM_PROJECTION, create_projection_matrix(quarter_pi, 1.333333f, 1.0f, 1000.0f));
	
	memset(coord_index, 0, MAX_TEXTURES * sizeof(int));

	for(int i=0; i<8; i++) {
		ttype[i] = TEX_2D;
	}

	if(sys_caps.point_params) {
		glPointParameterf(GL_POINT_SIZE_MIN_ARB, 1.0);
		glPointParameterf(GL_POINT_SIZE_MAX_ARB, 256.0);

		float quadratic[] = {0.0f, 0.0f, 0.01f};
		glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION_ARB, quadratic);
	}
}

const GraphicsInitParameters *get_graphics_init_parameters() {
	return &gparams;
}

void clear(const Color &color) {
	glClearColor(color.r, color.g, color.b, color.a);
	glClear(GL_COLOR_BUFFER_BIT);
}

void clear_zbuffer(scalar_t zval) {
	glClearDepth(zval);
	glClear(GL_DEPTH_BUFFER_BIT);
}

void clear_stencil(unsigned char sval) {
	glClearStencil(sval);
	glClear(GL_STENCIL_BUFFER_BIT);
}

void clear_zbuffer_stencil(scalar_t zval, unsigned char sval) {
	glClearDepth(zval);
	glClearStencil(sval);
	glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
}

void flip() {
	fxwt::swap_buffers();
}

void load_xform_matrices() {
	for(int i=0; i<sys_caps.max_texture_units; i++) {
		select_texture_unit(i);
		glMatrixMode(GL_TEXTURE);
		load_matrix_gl(tex_matrix[i]);
	}
	
	glMatrixMode(GL_PROJECTION);
	load_matrix_gl(proj_matrix);
	
	Matrix4x4 modelview = view_matrix * world_matrix;
	glMatrixMode(GL_MODELVIEW);
	load_matrix_gl(modelview);
}

#define BUFFER_OFFSET(i) ((char *)NULL + (i))

void draw(const VertexArray &varray) {
	load_xform_matrices();

	bool use_vbo = !varray.get_dynamic() && sys_caps.vertex_buffers;
	
	glEnableClientState(GL_VERTEX_ARRAY);
	glEnableClientState(GL_COLOR_ARRAY);
	glEnableClientState(GL_NORMAL_ARRAY);
	
	if(use_vbo) {
		Vertex v;
		glBindBuffer(GL_ARRAY_BUFFER_ARB, varray.get_buffer_object());
		glVertexPointer(3, GL_SCALAR_TYPE, sizeof(Vertex), (void*)((char*)&v.pos - (char*)&v));
		glNormalPointer(GL_SCALAR_TYPE, sizeof(Vertex), (void*)((char*)&v.normal - (char*)&v));
		glColorPointer(4, GL_SCALAR_TYPE, sizeof(Vertex), (void*)((char*)&v.color - (char*)&v));

		for(int i=0; i<MAX_TEXTURES; i++) {
			select_texture_unit(i);
			glEnableClientState(GL_TEXTURE_COORD_ARRAY);

			int dim = ttype[i] == TEX_1D ? 1 : (ttype[i] == TEX_3D || ttype[i] == TEX_CUBE ? 3 : 2);
			glTexCoordPointer(dim, GL_SCALAR_TYPE, sizeof(Vertex), (void*)((char*)&v.tex[coord_index[i]] - (char*)&v));
		}
	} else {
		glVertexPointer(3, GL_SCALAR_TYPE, sizeof(Vertex), &varray.get_data()->pos);
		glNormalPointer(GL_SCALAR_TYPE, sizeof(Vertex), &varray.get_data()->normal);
		glColorPointer(4, GL_SCALAR_TYPE, sizeof(Vertex), &varray.get_data()->color);

		for(int i=0; i<MAX_TEXTURES; i++) {
			select_texture_unit(i);
			glEnableClientState(GL_TEXTURE_COORD_ARRAY);

			int dim = ttype[i] == TEX_1D ? 1 : (ttype[i] == TEX_3D || ttype[i] == TEX_CUBE ? 3 : 2);
			glTexCoordPointer(dim, GL_SCALAR_TYPE, sizeof(Vertex), &varray.get_data()->tex[coord_index[i]]);
		}
	}
	
	glDrawArrays(primitive_type, 0, varray.get_count());
	
	if(use_vbo) glBindBuffer(GL_ARRAY_BUFFER_ARB, 0);
	
	glDisableClientState(GL_VERTEX_ARRAY);
	glDisableClientState(GL_COLOR_ARRAY);
	glDisableClientState(GL_NORMAL_ARRAY);
	
	for(int i=0; i<MAX_TEXTURES; i++) {
		select_texture_unit(i);
		glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	}
}

void draw(const VertexArray &varray, const IndexArray &iarray) {
	load_xform_matrices();
	
	bool use_vbo = !varray.get_dynamic() && sys_caps.vertex_buffers;
	bool use_ibo = false;//!iarray.get_dynamic() && sys_caps.vertex_buffers;
	
	glEnableClientState(GL_VERTEX_ARRAY);
	glEnableClientState(GL_COLOR_ARRAY);
	glEnableClientState(GL_NORMAL_ARRAY);
	
	if(use_vbo) {
		Vertex v;
		glBindBuffer(GL_ARRAY_BUFFER_ARB, varray.get_buffer_object());
		glVertexPointer(3, GL_SCALAR_TYPE, sizeof(Vertex), (void*)((char*)&v.pos - (char*)&v));
		glNormalPointer(GL_SCALAR_TYPE, sizeof(Vertex), (void*)((char*)&v.normal - (char*)&v));
		glColorPointer(4, GL_SCALAR_TYPE, sizeof(Vertex), (void*)((char*)&v.color - (char*)&v));

		for(int i=0; i<MAX_TEXTURES; i++) {
			select_texture_unit(i);
			glEnableClientState(GL_TEXTURE_COORD_ARRAY);

			int dim = ttype[i] == TEX_1D ? 1 : (ttype[i] == TEX_3D || ttype[i] == TEX_CUBE ? 3 : 2);
			glTexCoordPointer(dim, GL_SCALAR_TYPE, sizeof(Vertex), (void*)((char*)&v.tex[coord_index[i]] - (char*)&v));
		}
	} else {
		glVertexPointer(3, GL_SCALAR_TYPE, sizeof(Vertex), &varray.get_data()->pos);
		glNormalPointer(GL_SCALAR_TYPE, sizeof(Vertex), &varray.get_data()->normal);
		glColorPointer(4, GL_SCALAR_TYPE, sizeof(Vertex), &varray.get_data()->color);

		for(int i=0; i<MAX_TEXTURES; i++) {
			select_texture_unit(i);
			glEnableClientState(GL_TEXTURE_COORD_ARRAY);

			int dim = ttype[i] == TEX_1D ? 1 : (ttype[i] == TEX_3D || ttype[i] == TEX_CUBE ? 3 : 2);
			glTexCoordPointer(dim, GL_SCALAR_TYPE, sizeof(Vertex), &varray.get_data()->tex[coord_index[i]]);
		}
	}

	if(use_ibo) {
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, iarray.get_buffer_object());
		glDrawElements(primitive_type, iarray.get_count(), GL_UNSIGNED_SHORT, 0);
	} else {
		glDrawElements(primitive_type, iarray.get_count(), GL_UNSIGNED_SHORT, iarray.get_data());
	}
	
	if(use_ibo) glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
	if(use_vbo) glBindBuffer(GL_ARRAY_BUFFER_ARB, 0);
	
	glDisableClientState(GL_VERTEX_ARRAY);
	glDisableClientState(GL_COLOR_ARRAY);
	glDisableClientState(GL_NORMAL_ARRAY);
	
	for(int i=0; i<MAX_TEXTURES; i++) {
		select_texture_unit(i);
		glDisableClientState(GL_TEXTURE_COORD_ARRAY);
	}
}


/* draw_line(start_vertex, end_vertex, start_width, end_width)
 * Draws a line as a cylindrically billboarded elongated quad.
 */
void draw_line(const Vertex &v1, const Vertex &v2, scalar_t w1, scalar_t w2) {	
	if(w2 < 0.0) w2 = w1;

	Vector3 p1 = v1.pos;
	Vector3 p2 = v2.pos;

	Vector3 cam_pos = Vector3(0,0,0).transformed(inv_view_matrix);
	
	Vector3 vec = p2 - p1;
	scalar_t len = vec.length();
	
	Basis basis;
	basis.k = -(cam_pos - ((p2 + p1) / 2)).normalized();
	basis.j = vec / len;
	basis.i = cross_product(basis.j, basis.k).normalized();
	basis.k = cross_product(basis.i, basis.j).normalized();

	world_matrix.set_translation(p1);
	world_matrix = world_matrix * Matrix4x4(basis.create_rotation_matrix());
	load_xform_matrices();

	Vertex quad[] = {
		Vertex(Vector3(-w1, 0, 0), 0.0, v1.tex[0].u),
		Vertex(Vector3(-w2, len, 0), 0.0, v2.tex[0].u),
		Vertex(Vector3(w2, len, 0), 1.0, v2.tex[0].u),
		Vertex(Vector3(w1, 0, 0), 1.0, v1.tex[0].u)
	};

	set_lighting(false);
	set_primitive_type(QUAD_LIST);
	draw(VertexArray(quad, 4));
	set_primitive_type(TRIANGLE_LIST);
	set_lighting(true);
}

void draw_point(const Vertex &pt, scalar_t size) {

	Vector3 p = pt.pos;
	
	Vector3 cam_pos = Vector3(0,0,0).transformed(inv_view_matrix);

	Basis basis;
	basis.k = -(cam_pos - p).normalized();
	basis.j = Vector3(0, 1, 0);
	basis.i = cross_product(basis.j, basis.k);
	basis.j = cross_product(basis.k, basis.i);

	world_matrix.set_translation(p);
	world_matrix = world_matrix * Matrix4x4(basis.create_rotation_matrix());
	load_xform_matrices();

	Vertex quad[] = {
		Vertex(Vector3(-size, -size, 0), 0.0, 0.0, pt.color),
		Vertex(Vector3(-size, size, 0), 0.0, 1.0, pt.color),
		Vertex(Vector3(size, size, 0), 1.0, 1.0, pt.color),
		Vertex(Vector3(size, -size, 0), 1.0, 0.0, pt.color)
	};

	set_lighting(false);
	set_primitive_type(QUAD_LIST);
	draw(VertexArray(quad, 4));
	set_primitive_type(TRIANGLE_LIST);
	set_lighting(true);

}


void draw_scr_quad(const Vector2 &corner1, const Vector2 &corner2, const Color &color, bool reset_xform) {
	if(reset_xform) {
		glMatrixMode(GL_MODELVIEW);
		glPushMatrix();
		glLoadIdentity();
	}

	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	glOrtho(0.0, 1.0, 1.0, 0.0, 0.0, 1.0);

	glBegin(GL_QUADS);
	glColor4f(color.r, color.g, color.b, color.a);
	glTexCoord2f(0.0f, 1.0f);
	glVertex3f(corner1.x, corner1.y, -0.5);
	glTexCoord2f(1.0f, 1.0f);
	glVertex3f(corner2.x, corner1.y, -0.5);
	glTexCoord2f(1.0f, 0.0f);
	glVertex3f(corner2.x, corner2.y, -0.5);
	glTexCoord2f(0.0f, 0.0f);
	glVertex3f(corner1.x, corner2.y, -0.5);
	glEnd();

	glPopMatrix();

	if(reset_xform) {
		glMatrixMode(GL_MODELVIEW);
		glPopMatrix();
	}
}

int get_texture_unit_count() {
	return sys_caps.max_texture_units;
}

//////////////////// render states /////////////////////

void set_primitive_type(PrimitiveType pt) {
	primitive_type = pt;
}

void set_backface_culling(bool enable) {
	if(enable) {
		glEnable(GL_CULL_FACE);
	} else {
		glDisable(GL_CULL_FACE);
	}
}

void set_front_face(FaceOrder order) {
	glFrontFace(order);
}

void set_auto_normalize(bool enable) {
	if(enable) {
		glEnable(GL_NORMALIZE);
	} else {
		glDisable(GL_NORMALIZE);
	}
}

void set_color_write(bool red, bool green, bool blue, bool alpha) {
	glColorMask(red, green, blue, alpha);
}

void set_wireframe(bool enable) {
	//set_primitive_type(enable ? LINE_LIST : TRIANGLE_LIST);
	glPolygonMode(GL_FRONT, enable ? GL_LINE : GL_FILL);
}
	

///////////////// blending states ///////////////

void set_alpha_blending(bool enable) {
	if(enable) {
		glEnable(GL_BLEND);
	} else {
		glDisable(GL_BLEND);
	}
}

void set_blend_func(BlendingFactor src, BlendingFactor dest) {
	glBlendFunc(src, dest);
}

///////////////// zbuffer states ////////////////

void set_zbuffering(bool enable) {
	if(enable) {
		glEnable(GL_DEPTH_TEST);
	} else {
		glDisable(GL_DEPTH_TEST);
	}
}

void set_zwrite(bool enable) {
	glDepthMask(enable);
}

void set_zfunc(CmpFunc func) {
	glDepthFunc(func);
}

/////////////// stencil states //////////////////
void set_stencil_buffering(bool enable) {
	if(enable) {
		glEnable(GL_STENCIL_TEST);
	} else {
		glDisable(GL_STENCIL_TEST);
	}
}

void set_stencil_pass_op(StencilOp sop) {
	stencil_pass = sop;
	glStencilOp(stencil_fail, stencil_pzfail, stencil_pass);
}

void set_stencil_fail_op(StencilOp sop) {
	stencil_fail = sop;
	glStencilOp(stencil_fail, stencil_pzfail, stencil_pass);
}

void set_stencil_pass_zfail_op(StencilOp sop) {
	stencil_pzfail = sop;
	glStencilOp(stencil_fail, stencil_pzfail, stencil_pass);
}

void set_stencil_op(StencilOp fail, StencilOp spass_zfail, StencilOp pass) {
	stencil_fail = fail;
	stencil_pzfail = spass_zfail;
	stencil_pass = pass;
	glStencilOp(stencil_fail, stencil_pzfail, stencil_pass);
}

void set_stencil_func(CmpFunc func) {
	glStencilFunc(func, stencil_ref, 0xffffffff);
}

void set_stencil_reference(unsigned int ref) {
	stencil_ref = ref;
}

///////////// texture & material states //////////////

void set_point_sprites(bool enable) {
	if(enable) {
		glEnable(GL_POINT_SPRITE_ARB);
	} else {
		glDisable(GL_POINT_SPRITE_ARB);
	}
}

void set_texture_filtering(int tex_unit, TextureFilteringType tex_filter) {
	
	int min_filter;
	
	switch(tex_filter) {
	case POINT_SAMPLING:
		min_filter = mipmapping ? GL_NEAREST_MIPMAP_NEAREST : GL_NEAREST;
		glTexParameteri(ttype[tex_unit], GL_TEXTURE_MIN_FILTER, min_filter);
		glTexParameteri(ttype[tex_unit], GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		break;
		
	case BILINEAR_FILTERING:
		min_filter = mipmapping ? GL_LINEAR_MIPMAP_NEAREST : GL_LINEAR;
		glTexParameteri(ttype[tex_unit], GL_TEXTURE_MIN_FILTER, min_filter);
		glTexParameteri(ttype[tex_unit], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		break;
		
	case TRILINEAR_FILTERING:
	default:
		min_filter = mipmapping ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR;
		glTexParameteri(ttype[tex_unit], GL_TEXTURE_MIN_FILTER, min_filter);
		glTexParameteri(ttype[tex_unit], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		break;
	}
}

void set_texture_addressing(int tex_unit, TextureAddressing uaddr, TextureAddressing vaddr) {
	glTexParameteri(ttype[tex_unit], GL_TEXTURE_WRAP_S, uaddr);
	glTexParameteri(ttype[tex_unit], GL_TEXTURE_WRAP_T, vaddr);
}

void set_texture_border_color(int tex_unit, const Color &color) {
	float col[] = {color.r, color.g, color.b, color.a};
	glTexParameterfv(ttype[tex_unit], GL_TEXTURE_BORDER_COLOR, col);
}

void set_texture(int tex_unit, const Texture *tex) {
	select_texture_unit(tex_unit);
	glBindTexture(tex->get_type(), tex->tex_id);
	ttype[tex_unit] = tex->get_type();
}

void set_mip_mapping(bool enable) {
	mipmapping = enable;
}

void set_material(const Material &mat) {
	mat.set_glmaterial();
}

void use_vertex_colors(bool enable) {
	if(enable) {
		glEnable(GL_COLOR_MATERIAL);
	} else {
		glDisable(GL_COLOR_MATERIAL);
	}
}


void set_render_target(Texture *tex, CubeMapFace cube_map_face) {
	static std::stack<Texture*> rt_stack;
	static std::stack<CubeMapFace> face_stack;
	
	Texture *prev = rt_stack.empty() ? 0 : rt_stack.top();
	CubeMapFace prev_face = CUBE_MAP_PX; // just to get rid of the uninitialized var warning
	if(!face_stack.empty()) prev_face = face_stack.top();

	if(tex == prev) return;

	if(prev) {
		set_texture(0, prev);
		glCopyTexSubImage2D(prev->get_type() == TEX_CUBE ? prev_face : GL_TEXTURE_2D, 0, 0, 0, 0, 0, prev->width, prev->height);
	}
	
	if(!tex) {
		rt_stack.pop();
		if(prev->get_type() == TEX_CUBE) face_stack.pop();

		if(rt_stack.empty()) {
			set_viewport(0, 0, gparams.x, gparams.y);
		} else {
			set_viewport(0, 0, rt_stack.top()->width, rt_stack.top()->height);
		}
	} else {
		set_viewport(0, 0, tex->width, tex->height);

		rt_stack.push(tex);
		if(tex->get_type() == TEX_CUBE) face_stack.push(cube_map_face);
	}
}

// multitexturing interface

void select_texture_unit(int tex_unit) {
	glext::glActiveTexture(GL_TEXTURE0 + tex_unit);
	glext::glClientActiveTexture(GL_TEXTURE0 + tex_unit);
}

void enable_texture_unit(int tex_unit) {
	select_texture_unit(tex_unit);
	glEnable(ttype[tex_unit]);
}

void disable_texture_unit(int tex_unit) {
	select_texture_unit(tex_unit);
	glDisable(ttype[tex_unit]);
}

void set_texture_unit_color(int tex_unit, TextureBlendFunction op, TextureBlendArgument arg1, TextureBlendArgument arg2, TextureBlendArgument arg3) {
	
	select_texture_unit(tex_unit);
	glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
	glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, op);
	glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, arg1);
	glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, arg2);
	if(arg3 != TARG_NONE) {
		glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE2_RGB, arg3);
	}
}

void set_texture_unit_alpha(int tex_unit, TextureBlendFunction op, TextureBlendArgument arg1, TextureBlendArgument arg2, TextureBlendArgument arg3) {
	
	select_texture_unit(tex_unit);
	glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
	glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, op);
	glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, arg1);
	glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, arg2);
	if(arg3 != TARG_NONE) {
		glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE2_ALPHA, arg3);
	}
}

void set_texture_coord_index(int tex_unit, int index) {
	coord_index[tex_unit] = index;
}

void set_texture_constant(int tex_unit, const Color &col) {
	float color[] = {col.r, col.g, col.b, col.a};
	select_texture_unit(tex_unit);
	glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color);
}

//void set_texture_transform_state(int sttex_unitage, TexTransformState TexXForm);
//void set_texture_coord_generator(int stage, TexGen tgen);

void set_point_sprite_coords(int tex_unit, bool enable) {
	select_texture_unit(tex_unit);
	glTexEnvi(GL_POINT_SPRITE_ARB, GL_COORD_REPLACE_ARB, enable ? GL_TRUE : GL_FALSE);
}


// programmable interface
void set_gfx_program(GfxProg *prog) {
	if(prog) {
		if(!prog->linked) {
			prog->link();
			if(!prog->linked) return;
		}
		glUseProgramObject(prog->prog);
		
		// call any registered update handlers
		if(prog->update_handler) {
			prog->update_handler(prog);
		}
	} else {
		glUseProgramObject(0);
	}
}

// lighting states
void set_lighting(bool enable) {
	if(enable) {
		glEnable(GL_LIGHTING);
	} else {
		glDisable(GL_LIGHTING);
	}
}

void set_ambient_light(const Color &ambient_color) {
	float col[] = {ambient_color.r, ambient_color.g, ambient_color.b, ambient_color.a};
	glLightModelfv(GL_LIGHT_MODEL_AMBIENT, col);
}

void set_shading_mode(ShadeMode mode) {
	glShadeModel(mode);
}

void set_bump_light(const Light *light) {
	bump_light = light;
}

// transformation matrices
void set_matrix(TransformType xform_type, const Matrix4x4 &mat, int num) {
	switch(xform_type) {
	case XFORM_WORLD:
		world_matrix = mat;
		break;
		
	case XFORM_VIEW:
		view_matrix = mat;
		inv_view_matrix = view_matrix.inverse();
		view_mat_camera = 0;
		break;
		
	case XFORM_PROJECTION:
		proj_matrix = mat;
		break;
		
	case XFORM_TEXTURE:
		tex_matrix[num] = mat;
		break;
	}
}

Matrix4x4 get_matrix(TransformType xform_type, int num) {
	switch(xform_type) {
	case XFORM_WORLD:
		return world_matrix;
		
	case XFORM_VIEW:
		return view_matrix;
		
	case XFORM_TEXTURE:
		return tex_matrix[num];
		
	case XFORM_PROJECTION:
	default:
		return proj_matrix;
	}
}

void set_viewport(unsigned int x, unsigned int y, unsigned int xsize, unsigned int ysize) {
	glViewport(x, y, xsize, ysize);
}

Matrix4x4 create_projection_matrix(scalar_t vfov, scalar_t aspect, scalar_t near_clip, scalar_t far_clip) {
	
	scalar_t hfov = vfov * aspect;
	scalar_t w = 1.0f / (scalar_t)tan(hfov * 0.5f);
	scalar_t h = 1.0f / (scalar_t)tan(vfov * 0.5f);
	scalar_t q = far_clip / (far_clip - near_clip);
	
	Matrix4x4 mat;
	//mat.set_scaling(Vector4(w, h, q, 0));
	mat[0][0] = w;
	mat[1][1] = h;
	mat[2][2] = q;
	mat[3][2] = 1.0f;
	mat[2][3] = -q * near_clip;
	
	return mat;
}


// ---- misc ----

bool screen_capture(char *fname, enum image_file_format fmt) {
	static int scr_num;
	static const char *suffix[] = {"png", "jpg", "tga", "oug1", "oug2"};
	int x = gparams.x;
	int y = gparams.y;

	unsigned long *pixels = new unsigned long[x * y];
	glReadPixels(0, 0, x, y, GL_BGRA, GL_UNSIGNED_BYTE, pixels);
	
	if(!fname) {
		static char fname_buf[50];
		fname = fname_buf;
		sprintf(fname, "3dengfx_shot%04d.%s", scr_num++, suffix[fmt]);
	}

	unsigned int flags = get_image_save_flags();
	set_image_save_flags(flags | IMG_SAVE_INVERT);
	int res = save_image(fname, pixels, x, y, fmt);
	set_image_save_flags(flags);
	
	delete [] pixels;
	return res != -1;
}
